package com.gmail.volodymyrdotsenko.roo.webeasy.packaging;

import static org.springframework.roo.model.RooJavaType.ROO_EQUALS;
import static org.springframework.roo.model.RooJavaType.ROO_JAVA_BEAN;
import static org.springframework.roo.model.RooJavaType.ROO_JPA_ACTIVE_RECORD;
import static org.springframework.roo.model.RooJavaType.ROO_JPA_ENTITY;
import static org.springframework.roo.model.RooJavaType.ROO_SERIALIZABLE;
import static org.springframework.roo.model.RooJavaType.ROO_TO_STRING;
import static org.springframework.roo.project.Path.SPRING_CONFIG_ROOT;
import static org.springframework.roo.project.Path.SRC_MAIN_JAVA;
import static org.springframework.roo.project.Path.SRC_MAIN_WEBAPP;
import static org.springframework.roo.project.Path.SRC_TEST_JAVA;
import static org.springframework.roo.project.Path.SRC_TEST_RESOURCES;
import static org.springframework.roo.model.JavaType.LONG_OBJECT;
import static org.springframework.roo.model.SpringJavaType.CONTROLLER;
import static org.springframework.roo.model.SpringJavaType.REQUEST_MAPPING;
import static org.springframework.roo.model.SpringJavaType.MODEL;
import static org.springframework.roo.model.JavaType.STRING;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.springframework.roo.classpath.PhysicalTypeCategory;
import org.springframework.roo.classpath.PhysicalTypeIdentifier;
import org.springframework.roo.classpath.TypeManagementService;
import org.springframework.roo.classpath.details.ClassOrInterfaceTypeDetailsBuilder;
import org.springframework.roo.classpath.details.FieldMetadata;
import org.springframework.roo.classpath.details.FieldMetadataBuilder;
import org.springframework.roo.classpath.details.MethodMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.AnnotatedJavaType;
import org.springframework.roo.classpath.details.annotations.AnnotationAttributeValue;
import org.springframework.roo.classpath.details.annotations.AnnotationMetadataBuilder;
import org.springframework.roo.classpath.details.annotations.StringAttributeValue;
import org.springframework.roo.classpath.itd.InvocableMemberBodyBuilder;
import org.springframework.roo.classpath.operations.InheritanceType;
import org.springframework.roo.classpath.operations.jsr303.FieldDetails;
import org.springframework.roo.metadata.MetadataService;
import org.springframework.roo.model.JavaPackage;
import org.springframework.roo.model.JavaSymbolName;
import org.springframework.roo.model.JavaType;
import org.springframework.roo.project.GAV;
import org.springframework.roo.project.Path;
import org.springframework.roo.project.ProjectMetadata;
import org.springframework.roo.project.ProjectOperations;
import org.springframework.roo.project.packaging.AbstractPackagingProvider;
import org.springframework.roo.support.util.DomUtils;
import org.springframework.roo.support.util.FileUtils;
import org.springframework.roo.support.util.XmlUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

@Component
@Service
public class WebeasyPackagingProvider extends AbstractPackagingProvider {

	public static final String NAME = "webeasy";

	public WebeasyPackagingProvider() {
		super("webeasy", "war", "war-easy-pom-template.xml");
	}

	@Override
	protected void createOtherArtifacts(final JavaPackage topLevelPackage,
			final String module, final ProjectOperations projectOperations) {
		super.createOtherArtifacts(topLevelPackage, module, projectOperations);
		if (StringUtils.isBlank(module)) {
			// This is a single-module web project
			final String fullyQualifiedModuleName = getFullyQualifiedModuleName(
					module, projectOperations);
			// applicationContextOperations.createMiddleTierApplicationContext(
			// topLevelPackage, fullyQualifiedModuleName);
			// spring config
			buildContextXml(topLevelPackage, fullyQualifiedModuleName);
			// database config
			setUpDatabaseConfiguration();
			setUpPersistenceConfiguration();
			// second-level cache config
			setUpEhcacheConfiguration();

			// email config
			setUpEmailConfiguration();

			// web app config
			setUpWebConfiguration();
			buildWebMvcConfig(topLevelPackage);

			// spring security config
			setUpSpringSecurityConfiguration();

			// build entity example
			buildEntityExample(topLevelPackage);

			// build controller example
			buildControllerExample(topLevelPackage);
		}
	}

	private static final AnnotationMetadataBuilder ROO_JAVA_BEAN_BUILDER = new AnnotationMetadataBuilder(
			ROO_JAVA_BEAN);
	private static final AnnotationMetadataBuilder ROO_SERIALIZABLE_BUILDER = new AnnotationMetadataBuilder(
			ROO_SERIALIZABLE);
	private static final AnnotationMetadataBuilder ROO_TO_STRING_BUILDER = new AnnotationMetadataBuilder(
			ROO_TO_STRING);

	@Reference
	TypeManagementService typeManagementService;

	private void buildEntityExample(final JavaPackage topLevelPackage) {
		// Create entity's annotations
		final List<AnnotationMetadataBuilder> annotationBuilder = new ArrayList<AnnotationMetadataBuilder>();
		annotationBuilder.add(ROO_JAVA_BEAN_BUILDER);
		annotationBuilder.add(ROO_TO_STRING_BUILDER);
		annotationBuilder.add(getEntityAnnotationBuilder("projects", null,
				null, null, null, LONG_OBJECT, null, null, JavaType.INT_OBJECT,
				null, false, null, null, null, null, true));
		annotationBuilder.add(ROO_SERIALIZABLE_BUILDER);

		// Produce the entity itself
		JavaType name = new JavaType(
				topLevelPackage.getFullyQualifiedPackageName()
						+ ".domain.Project");

		final String declaredByMetadataId = PhysicalTypeIdentifier
				.createIdentifier(name,
						pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA));
		final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder(
				declaredByMetadataId, Modifier.PUBLIC, name,
				PhysicalTypeCategory.CLASS);

		cidBuilder.setAnnotations(annotationBuilder);

		final List<FieldMetadataBuilder> fieldBuilder = new ArrayList<FieldMetadataBuilder>();

		fieldBuilder.add(new FieldMetadataBuilder(PhysicalTypeIdentifier
				.createIdentifier(
						new JavaType(topLevelPackage
								.getFullyQualifiedPackageName()
								+ ".domain.Project.name"), pathResolver
								.getFocusedPath(Path.SRC_MAIN_JAVA)),
				Modifier.PRIVATE, new ArrayList<AnnotationMetadataBuilder>(),
				new JavaSymbolName("sampleField"), JavaType.STRING));
		cidBuilder.setDeclaredFields(fieldBuilder);

		typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build());
	}

	private void buildControllerExample(final JavaPackage topLevelPackage) {
		// Create controller's annotations
		final List<AnnotationMetadataBuilder> annotationBuilder = new ArrayList<AnnotationMetadataBuilder>();

		annotationBuilder.add(new AnnotationMetadataBuilder(CONTROLLER));

		// Create annotation @RequestMapping("/myobject/**")
		final List<AnnotationAttributeValue<?>> requestMappingAttributes = new ArrayList<AnnotationAttributeValue<?>>();
		requestMappingAttributes.add(new StringAttributeValue(
				new JavaSymbolName("value"), "/start"));
		annotationBuilder.add(new AnnotationMetadataBuilder(REQUEST_MAPPING,
				requestMappingAttributes));

		// Produce the entity itself
		JavaType name = new JavaType(
				topLevelPackage.getFullyQualifiedPackageName()
						+ ".web.StartController");

		final String declaredByMetadataId = PhysicalTypeIdentifier
				.createIdentifier(name,
						pathResolver.getFocusedPath(Path.SRC_MAIN_JAVA));
		final ClassOrInterfaceTypeDetailsBuilder cidBuilder = new ClassOrInterfaceTypeDetailsBuilder(
				declaredByMetadataId, Modifier.PUBLIC, name,
				PhysicalTypeCategory.CLASS);

		cidBuilder.setAnnotations(annotationBuilder);

		final List<MethodMetadataBuilder> methodBuilder = new ArrayList<MethodMetadataBuilder>();

		methodBuilder.add(buildStartMethod(topLevelPackage));

		cidBuilder.setDeclaredMethods(methodBuilder);

		typeManagementService.createOrUpdateTypeOnDisk(cidBuilder.build());
	}

	private MethodMetadataBuilder buildStartMethod(
			final JavaPackage topLevelPackage) {
		final JavaSymbolName methodName = new JavaSymbolName("index");

		final List<JavaType> parameterTypes = Arrays.asList(MODEL);
		final List<JavaSymbolName> parameterNames = Arrays
				.asList(new JavaSymbolName("uiModel"));

		final List<AnnotationMetadataBuilder> annotations = new ArrayList<AnnotationMetadataBuilder>();

		final List<AnnotationAttributeValue<?>> requestMappingAttributes = new ArrayList<AnnotationAttributeValue<?>>();
		// requestMappingAttributes.add(new StringAttributeValue(
		// new JavaSymbolName("params"), "form"));
		// requestMappingAttributes.add(PRODUCES_HTML);
		final AnnotationMetadataBuilder requestMapping = new AnnotationMetadataBuilder(
				REQUEST_MAPPING, requestMappingAttributes);
		annotations.add(requestMapping);

		final InvocableMemberBodyBuilder bodyBuilder = new InvocableMemberBodyBuilder();
		bodyBuilder.appendFormalLine("return \"start\";");

		final MethodMetadataBuilder methodBuilder = new MethodMetadataBuilder(
				PhysicalTypeIdentifier.createIdentifier(new JavaType(
						topLevelPackage.getFullyQualifiedPackageName()
								+ ".web.StartController"), pathResolver
						.getFocusedPath(Path.SRC_MAIN_JAVA)), Modifier.PUBLIC,
				methodName, STRING,
				AnnotatedJavaType.convertFromJavaTypes(parameterTypes),
				parameterNames, bodyBuilder);
		methodBuilder.setAnnotations(annotations);
		return methodBuilder;
	}

	/**
	 * Returns a builder for the entity-related annotation to be added to a
	 * newly created JPA entity
	 * 
	 * @param table
	 * @param schema
	 * @param catalog
	 * @param identifierField
	 * @param identifierColumn
	 * @param identifierType
	 * @param versionField
	 * @param versionColumn
	 * @param versionType
	 * @param inheritanceType
	 * @param mappedSuperclass
	 * @param persistenceUnit
	 * @param transactionManager
	 * @param entityName
	 * @param sequenceName
	 * @param activeRecord
	 *            whether to generate active record CRUD methods for the entity
	 * @return a non-<code>null</code> builder
	 */
	private AnnotationMetadataBuilder getEntityAnnotationBuilder(
			final String table, final String schema, final String catalog,
			final String identifierField, final String identifierColumn,
			final JavaType identifierType, final String versionField,
			final String versionColumn, final JavaType versionType,
			final InheritanceType inheritanceType,
			final boolean mappedSuperclass, final String persistenceUnit,
			final String transactionManager, final String entityName,
			final String sequenceName, final boolean activeRecord) {

		final AnnotationMetadataBuilder entityAnnotationBuilder = new AnnotationMetadataBuilder(
				getEntityAnnotationType(activeRecord));

		// Attributes that apply to all JPA entities (active record or not)
		if (catalog != null) {
			entityAnnotationBuilder.addStringAttribute("catalog", catalog);
		}
		if (entityName != null) {
			entityAnnotationBuilder
					.addStringAttribute("entityName", entityName);
		}
		if (sequenceName != null) {
			entityAnnotationBuilder.addStringAttribute("sequenceName",
					sequenceName);
		}
		if (identifierColumn != null) {
			entityAnnotationBuilder.addStringAttribute("identifierColumn",
					identifierColumn);
		}
		if (identifierField != null) {
			entityAnnotationBuilder.addStringAttribute("identifierField",
					identifierField);
		}
		if (!LONG_OBJECT.equals(identifierType)) {
			entityAnnotationBuilder.addClassAttribute("identifierType",
					identifierType);
		}
		if (inheritanceType != null) {
			entityAnnotationBuilder.addStringAttribute("inheritanceType",
					inheritanceType.name());
		}
		if (mappedSuperclass) {
			entityAnnotationBuilder.addBooleanAttribute("mappedSuperclass",
					mappedSuperclass);
		}
		if (schema != null) {
			entityAnnotationBuilder.addStringAttribute("schema", schema);
		}
		if (table != null) {
			entityAnnotationBuilder.addStringAttribute("table", table);
		}
		// if (versionColumn != null
		// && !RooJpaEntity.VERSION_COLUMN_DEFAULT.equals(versionColumn)) {
		// entityAnnotationBuilder.addStringAttribute("versionColumn",
		// versionColumn);
		// }
		// if (versionField != null
		// && !RooJpaEntity.VERSION_FIELD_DEFAULT.equals(versionField)) {
		// entityAnnotationBuilder.addStringAttribute("versionField",
		// versionField);
		// }
		if (!JavaType.INT_OBJECT.equals(versionType)) {
			entityAnnotationBuilder.addClassAttribute("versionType",
					versionType);
		}

		// Attributes that only apply to entities with CRUD active record
		// methods
		if (activeRecord) {
			if (persistenceUnit != null) {
				entityAnnotationBuilder.addStringAttribute("persistenceUnit",
						persistenceUnit);
			}
			if (transactionManager != null) {
				entityAnnotationBuilder.addStringAttribute(
						"transactionManager", transactionManager);
			}
		}

		return entityAnnotationBuilder;
	}

	/**
	 * Returns the type of annotation to put on the entity
	 * 
	 * @param activeRecord
	 *            whether the entity is to have CRUD active record methods
	 *            generated
	 * @return a non-<code>null</code> type
	 */
	private JavaType getEntityAnnotationType(final boolean activeRecord) {
		return activeRecord ? ROO_JPA_ACTIVE_RECORD : ROO_JPA_ENTITY;
	}

	@Reference
	private MetadataService metadataService;

	private void buildContextXml(final JavaPackage topLevelPackage,
			final String moduleName) {

		final ProjectMetadata projectMetadata = (ProjectMetadata) metadataService
				.get(ProjectMetadata.getProjectIdentifier(moduleName));
		Validate.notNull(projectMetadata,
				"Project metadata required for module '%s'", moduleName);
		final Document document = XmlUtils.readXml(FileUtils.getInputStream(
				getClass(), "applicationContext-template.xml"));
		final Element root = document.getDocumentElement();
		DomUtils.findFirstElementByName("context:component-scan", root)
				.setAttribute("base-package",
						topLevelPackage.getFullyQualifiedPackageName());
		fileManager.createOrUpdateTextFileIfRequired(pathResolver
				.getIdentifier(
						Path.SPRING_CONFIG_ROOT.getModulePathId(moduleName),
						"applicationContext.xml"), XmlUtils
				.nodeToString(document), false);
		fileManager.scan();
	}

	private void setUpEmailConfiguration() {
		final String emailConfigFile = pathResolver.getFocusedIdentifier(
				Path.SPRING_CONFIG_ROOT, "email.properties");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "email.properties-template");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(emailConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install email.properties file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	private void setUpDatabaseConfiguration() {
		final String databaseConfigFile = pathResolver.getFocusedIdentifier(
				Path.SPRING_CONFIG_ROOT, "database.properties");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "database.properties-template");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(databaseConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install database.properties file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	private void setUpPersistenceConfiguration() {
		final String persistenceConfigFile = pathResolver.getFocusedIdentifier(
				Path.SRC_MAIN_RESOURCES, "META-INF/persistence.xml");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "persistence-template.xml");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(persistenceConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install persistence.xml file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	private void setUpEhcacheConfiguration() {
		final String ehcacheConfigFile = pathResolver.getFocusedIdentifier(
				Path.SRC_MAIN_RESOURCES, "META-INF/ehcache.xml");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "ehcache-template.xml");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(ehcacheConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install ehcacheConfigFile.xml file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	private void setUpWebConfiguration() {
		final String webConfigFile = pathResolver.getFocusedIdentifier(
				Path.SRC_MAIN_WEBAPP, "WEB-INF/web.xml");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "web-template.xml");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(webConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install web.xml file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	private void buildWebMvcConfig(final JavaPackage topLevelPackage) {
		final Document document = XmlUtils.readXml(FileUtils.getInputStream(
				getClass(), "webmvc-config-template.xml"));
		final Element root = document.getDocumentElement();
		DomUtils.findFirstElementByName("context:component-scan", root)
				.setAttribute("base-package",
						topLevelPackage.getFullyQualifiedPackageName());
		fileManager.createOrUpdateTextFileIfRequired(pathResolver
				.getFocusedIdentifier(Path.SRC_MAIN_WEBAPP,
						"WEB-INF/spring/webmvc-config.xml"), XmlUtils
				.nodeToString(document), false);
		fileManager.scan();
	}

	private void setUpSpringSecurityConfiguration() {
		final String secConfigFile = pathResolver.getFocusedIdentifier(
				Path.SPRING_CONFIG_ROOT, "applicationContext-security.xml");
		final InputStream templateInputStream = FileUtils.getInputStream(
				getClass(), "applicationContext-security-template.xml");
		OutputStream outputStream = null;
		try {
			outputStream = fileManager.createFile(secConfigFile)
					.getOutputStream();
			IOUtils.copy(templateInputStream, outputStream);
		} catch (final IOException e) {
			LOGGER.warning("Unable to install web.xml file");
		} finally {
			IOUtils.closeQuietly(templateInputStream);
			IOUtils.closeQuietly(outputStream);
		}
	}

	@Override
	protected String createPom(final JavaPackage topLevelPackage,
			final String nullableProjectName, final String javaVersion,
			final GAV parentPom, final String moduleName,
			final ProjectOperations projectOperations) {

		final String pomPath = super.createPom(topLevelPackage,
				nullableProjectName, javaVersion, parentPom, moduleName,
				projectOperations);
		return pomPath;
	}

	public Collection<Path> getPaths() {
		return Arrays.asList(SRC_MAIN_JAVA, SRC_TEST_JAVA, SRC_TEST_RESOURCES,
				SPRING_CONFIG_ROOT, SRC_MAIN_WEBAPP);
	}

	@Override
	public boolean isDefault() {
		// TODO Auto-generated method stub
		return true;
	}
}